package agent {
import mx.controls.SWFLoader;
import mx.collections.ArrayCollection;

import automation.SeleFxAutomation;
import flash.display.DisplayObjectContainer;
import flash.display.DisplayObject;
import flash.events.*;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.display.DisplayObject;
import flash.utils.getDefinitionByName;

import flash.system.Capabilities;
import mx.events.FlexEvent;
import mx.automation.Automation;
import mx.automation.AutomationError;
import mx.automation.AutomationID;
import mx.automation.AutomationIDPart;
import mx.automation.IAutomationClass;
import mx.automation.IAutomationManager;
import mx.automation.IAutomationObject;
import mx.automation.IAutomationTabularData;
import mx.automation.IAutomationMethodDescriptor;
import mx.automation.IAutomationEventDescriptor;
import mx.automation.IAutomationPropertyDescriptor;

import mx.automation.events.AutomationRecordEvent;
import mx.core.Application;
import mx.core.mx_internal;
import mx.core.UIComponent;
import mx.utils.StringUtil;
import mx.automation.events.AutomationReplayEvent;
import mx.controls.Button;
import mx.managers.PopUpManager;
import flash.xml.XMLNode;
import mx.controls.Alert;

use namespace mx_internal;

[Mixin]
public class SeleFxAgent {

    public static var UIAgent:SeleFxAgent;

    private static var _root:DisplayObject;
    private static var isInitailized:Boolean = false;

    private var records:XML = <Records></Records>;

    private var seleEnv:SeleFxEnvironment;

    public static function init(root:DisplayObject):void {
        if (UIAgent == null) {
            _root = root;
            _root.addEventListener(FlexEvent.APPLICATION_COMPLETE, applicationCompleteHandler);
        }
    }

    private static function applicationCompleteHandler(event:FlexEvent):void {
        _root.removeEventListener(FlexEvent.APPLICATION_COMPLETE, applicationCompleteHandler);
        UIAgent = new SeleFxAgent();
    }

    public function SeleFxAgent() {
        if (! isInitailized) {
            var te:String = "/selenium-server/core/SeleFxAgentEnv.xml";

            var loader:URLLoader = new URLLoader();
            configureListeners(loader);

            var request:URLRequest = new URLRequest(te);
            try {
                loader.load(request);
            } catch (error:Error) {
                Alert.show("Unable to load SeleFxEnv.xml from current directory.");
            }

            isInitailized = true;
        }
    }


    public function get automationManager():IAutomationManager {
        return Automation.automationManager;
    }

    private function configureListeners(dispatcher:IEventDispatcher):void {
        dispatcher.addEventListener(Event.COMPLETE, completeHandler);
        dispatcher.addEventListener(Event.OPEN, openHandler);
        dispatcher.addEventListener(ProgressEvent.PROGRESS, progressHandler);
        dispatcher.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
        dispatcher.addEventListener(HTTPStatusEvent.HTTP_STATUS, httpStatusHandler);
        dispatcher.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
    }

    private function completeHandler(event:Event):void {
        var loader:URLLoader = URLLoader(event.target);
        setTestingEnvironment(loader.data);
        PopUpManager.createPopUp(DisplayObject(Application.application), SeleFxToolBar);
    }

    public function setTestingEnvironment(te:String):void {
        automationManager.automationEnvironment = new SeleFxEnvironment(new XML(te));
    }

    private function openHandler(event:Event):void {
    }

    private function progressHandler(event:ProgressEvent):void {
    }

    private function securityErrorHandler(event:SecurityErrorEvent):void {
        Alert.show("securityErrorHandler: " + event);
    }

    private function httpStatusHandler(event:HTTPStatusEvent):void {
    }

    private function ioErrorHandler(event:IOErrorEvent):void {
        Alert.show("ioErrorHandler: " + event);
    }

    /*** Toolbar Functions ***/
    public function beginRecording():Object {
        return useErrorHandler(function():Object {
            var o:Object = { result:null, error:0 };
            automationManager.addEventListener(AutomationRecordEvent.RECORD,
                    recordHandler, false, 0, true);
            automationManager.beginRecording();
            return o;
        });
    }

    public function endRecording():Object {
        return useErrorHandler(function():Object {
            var o:Object = { result:null, error:0 };
            automationManager.endRecording();
            automationManager.removeEventListener(AutomationRecordEvent.RECORD,
                    recordHandler);
            return o;
        });
    }

    public function getRecords():String {
        return records.toXMLString();
    }

    public function run(objID:String, method:String, args:String):Object {
        return useErrorHandler(function():Object
        {
            var rid:AutomationID = AutomationID.parse(objID);
            var target:IAutomationObject = automationManager.resolveIDToSingleObject(rid);

            if (!target)
                return { result:null, error:0 };


            return  replay(target, method, convertArrayFromStringToAs(args));
        });
    }

    public function getTabularData(objID:String, begin:uint = 0, end:uint = 0):Object
    {
        return useErrorHandler(function():Object
        {
            var o:Object = { result:null, error:0 };
            var rid:AutomationID = AutomationID.parse(objID);
            var obj:IAutomationObject = automationManager.resolveIDToSingleObject(rid);
            var td:IAutomationTabularData = automationManager.getTabularData(obj);
            o.result = {
                columnTitles: td ? td.columnNames : [],
                tableData: (td
                        ? td.getValues(begin, end)
                        : [[]])
            };
            return o;
        });
    }

    private function convertArrayFromStringToAs(a:String):Array {
        var result:Array = a.split("_ARG_SEP_");
        return result;
    }

    private function useErrorHandler(f:Function):Object {
        var o:Object = { result:null, error:0 };
        try {
            automationManager.incrementCacheCounter();
            o = f();
            automationManager.decrementCacheCounter();
        } catch (e:Error) {
            automationManager.decrementCacheCounter();
            o.error = (e is AutomationError
                    ? AutomationError(e).code
                    : AutomationError.ILLEGAL_OPERATION);
            trace(e.message);
        }
        return o;
    }

    public function getHierarchy(obj:IAutomationObject):String
    {
        var parent:UIComponent = UIComponent(obj);
        var event:String;
        var temp:String = parent.className + "(\"" + parent.automationName + "\").";
        while (Automation.automationManager.getParent(parent) != null)
        {
            parent = UIComponent(Automation.automationManager.getParent(parent));
            event = parent.className + "(\"" + parent.automationName + "\").";
            event = event.concat(temp);
            temp = event;
        }
        return event;
    }

    private function recordHandler(event:AutomationRecordEvent):void {
        automationManager.incrementCacheCounter();

        try {
            var obj:IAutomationObject = event.automationObject ;
            var rid:AutomationID = automationManager.createID(obj);

            var rec:XML = (<Step id={rid.toString()} method={event.name} ></Step>);

            XML.prettyPrinting = false;

            rec.appendChild(<Args value={event.args.join("_ARG_SEP_")} />);

            records.appendChild(rec);

        } catch (e:Error) {
            trace(e.message);
        }
        automationManager.decrementCacheCounter();
    }

    public function replay(target:IAutomationObject, method:String, args:Array):Object {
        automationManager.incrementCacheCounter();
        var o:Object = { result:null, error:0 };
        try {
            o.result = replayMethod(target, method, args);
        } catch(e:Error) {
            try {
                o.result = replayEvent(target, method, args);
            } catch(e:Error) {
                automationManager.decrementCacheCounter();
                throw e;
            }
        }
        automationManager.decrementCacheCounter(true);
        return o;
    }

    private function replayMethod(target:IAutomationObject, method:String, args:Array):Object {
        var automationClass:IAutomationClass =
                automationManager.automationEnvironment.getAutomationClassByInstance(target);
        // try to find the automation class
        if (! automationClass)
            throw new Error(SeleFxAutomation.getClassName(target) + "class not found");

        var methodDescriptor:IAutomationMethodDescriptor =
                automationClass.getDescriptorForMethodByName(method);

        if (!methodDescriptor)
            throw new Error(method + " method not found for " + automationClass);

        var retValue:Object = methodDescriptor.replay(target, args);

        if (retValue is IAutomationObject)
            retValue = automationManager.createID(IAutomationObject(retValue)).toString();

        return {value:retValue, type:methodDescriptor.returnType};
    }

    private function replayEvent(target:IAutomationObject, eventName:String, args:Array):Object {
        var automationClass:IAutomationClass =
                automationManager.automationEnvironment.getAutomationClassByInstance(target);
        if (! automationClass)
            throw new Error(SeleFxAutomation.getClassName(target) + "class not found");

        var eventDescriptor:IAutomationEventDescriptor =
                automationClass.getDescriptorForEventByName(eventName);

        if (!eventDescriptor)
            throw new Error(eventName + " event description not found for " + automationClass);

        var retValue:Object = eventDescriptor.replay(target, args);
        return {value:retValue, type:null};
    }

    public function getValueOf(objID:String, fieldName:String):Object {
        var rid:AutomationID = AutomationID.parse(objID);
        var target:IAutomationObject = automationManager.resolveIDToSingleObject(rid);
        if (target && Object(target).hasOwnProperty(fieldName)) {
            var returnValue:Object = Object(target)[ fieldName ];
            return { result:returnValue, error:0};
        } else {
            return { result:null, error:1};
        }
    }

    public function getElementsById(parentID:String, name:String):String {
        var collection:ArrayCollection = findObjects(parentID, name);
        if (collection == null || collection.length == 0) return "Unable to Find Flex Component for id/name =" + name;
        else {
            var tmp:String = new String();
            tmp = createID(collection[0]);
            for (var i:int = 1; i < collection.length; i++) {
                tmp = tmp + "#" + createID(collection[i]);
            }
            return tmp;
        }
    }

    /**
     * A help method to create the ID for the object. It is created to ease testing.
     * @param obj The display object
     * @return The id created with the help from AutomationID
     *
     * @see mx.automation.AutomationID
     */
    public function createID(obj:IAutomationObject):String {
        return automationManager.createID(obj).toString();
    }

    /**
     * Uses the objectTree string, which is a comma separated
     * string, to go through the display object hierarchy to find the wanted object.
     * It uses the methods startSearch and performSearch to achieve this goal.
     *
     * @param parentID The id of the parent object, String/AutomationID
     * @param name The name of the object
     * @return The object found, if noe object found it returns null
     *
     * @see startSearch
     * @see performSearch
     * @see mx.automation.AutomationID
     */
    public function findObjects(parentID:String, name:String):ArrayCollection {
        if (name == null) return null;

        if (parentID == null)
            return startSearch(name);
        else
            return performSearch(new Array(resolveObjFromID(parentID)), name);
    }

    protected function get	applicationRoot():Object {
        return _root;
    }

    /**
     * A helper method that is used to find the first display object from
     * the System manager. After the first object is discovered, it uses the performSearch method
     * to locate the next objects
     *
     * @param name The name of the object
     * @return The object found or null
     *
     * @see performSearch
     */
		public function startSearch(name:String):ArrayCollection {
			var parent:DisplayObjectContainer = DisplayObjectContainer(applicationRoot);
			var openList:Array = new Array();
            var runtimeloading:UIComponent = UIComponent(parent.getChildAt(0));
            var myLoader:SWFLoader = runtimeloading.getChildAt(0) as SWFLoader;
            var actualContainer:DisplayObjectContainer = myLoader.content as DisplayObjectContainer;
            for(var i:int=0; i<actualContainer.numChildren; i++) {
				try {
					openList.push(UIComponent(actualContainer.getChildAt(i)));
				}
				catch(e:Error){
                    trace(e);
                }
			}            
            return performSearch(openList, name);
		}

    /**
     * Uses a breadth first algorithm to find the object. The closest
     * objects are put in an openlist and the investigated items that are not the one
     * searched for are put in a closed list. It moves down in the hierarchy until the object
     * is found. The method checkName is used to see if the object is correct or not.
     *
     * @param openList The list of objects not investigated
     * @param name The name of the object
     * @return The object found, or null if not found.
     *
     * @see checkName
     */

    protected function performSearch(openList:Array, name:String):ArrayCollection
    {
        var foundChildren:ArrayCollection = new ArrayCollection();

        trace("Open List "+ openList.length);

        var check:Boolean = true;
        while(openList.length != 0)
        {
            try
            {
                var obj:Object  = openList.shift();
                var child:UIComponent = UIComponent(obj);
            }
            catch(e:Error)
            {
                trace("obj " + obj + " child " + child);
            }
             if(checkName(child, name))
                 foundChildren.addItem(child);

             for(var i:int=0; i<child.numChildren; i++)
             {
                 try
                {
                    var element:DisplayObject = child.getChildAt(i);
                    if (element is UIComponent) {
                        openList.push(UIComponent(element));
                    }

                 }
                 catch(e:Error)
                 {trace("Error .." + e);}
             }
         }
         return foundChildren;
     }


    /**
     * Takes a display object and a name, and checks different values
     * on the object to see if it matches the name.
     *
     * @param obj The display object
     * @param name The name of the desired display object
     * @return Indicating that the display object is correct or not
     *
     */
    public function checkName(obj:UIComponent, name:String):Boolean {
        if(obj.id == "myLoader") {
            
        }
        trace(" id :" + obj.id +" name : "+ obj.name + " automationName : " + obj.automationName);
        if (obj.id == name) return true;
        if (obj.name == name) return true;
        if (obj.automationName == name) return true;
        return false;
    }

		/**
		 * Method that resolves the object from the cerated ID
		 *
		 * @param objectID The id of the object, String/AutomationID
		 * @return The object resolved from the ID.
		 *
		 * @see mx.automation.AutomationID
		 */
		public function resolveObjFromID(objectID:String):UIComponent
		{
			var objArray:Array = resolveID(objectID);
			return UIComponent(objArray[0]);
		}

		/**
		 * Helper method for resolving the object from the ID. Made to ease testing.
		 *
		 * @param objectID The id of a object created with mx.automation.AutomationID
		 * @return Array
		 *
		 * @see mx.automation.AutomationID
		 */
		protected function resolveID(objectID:String):Array
		{
			return automationManager.resolveID(AutomationID.parse(objectID));
		}

}
}